﻿using System;
using System.Runtime.InteropServices;
using System.Drawing;

namespace DirectDrawWrapper
{
    public class DirectDrawGraphics : IDisposable
    {
        private const int DD_OK = 0;

        private IDirectDraw _ddraw;
        private IDirectDrawClipper _clipper;
        private Surface _primarySurface;
        private IntPtr _hwnd;

        private bool _supportHardwareFlip;
        private tagRECT _screenArea;
        private bool _isDisposed;
        private SurfaceCapsFlags _surfaceFlags;

        private void SetSupportFlip(BackbufferMode backBufferMode, DDCAPS driverCaps)
        {
            _supportHardwareFlip = (driverCaps.ddsCaps.dwCaps & SurfaceCapsFlags.BACKBUFFER) == SurfaceCapsFlags.BACKBUFFER;
            _supportHardwareFlip &= ((driverCaps.ddsCaps.dwCaps & SurfaceCapsFlags.FLIP) == SurfaceCapsFlags.FLIP);
            _supportHardwareFlip &= (backBufferMode != BackbufferMode.None);

            if (!_supportHardwareFlip && backBufferMode == BackbufferMode.Hardware)
                throw new NotSupportedException("Device does not support the minimum hardware options.");
        }

        private void CreateDirectDraw()
        {
            IDirectDraw draw;
            
            var result = NativeMethods.DirectDrawCreate(IntPtr.Zero, out draw, IntPtr.Zero);
            _ddraw = draw;
            if (result != DD_OK)
                throw ExceptionUtil.Create(result);
        }

        private void SetDirectDrawCoopLevel()
        {
            var result = _ddraw.SetCooperativeLevel(_hwnd, CooperativeFlags.Fullscreen);
            if (result != DD_OK)
                throw ExceptionUtil.Create(result);
        }

        private DDSURFACEDESC GetDesc()
        {
            var desc = new DDSURFACEDESC { dwFlags = SurfaceDescFlags.CAPS };
            desc.ddsCaps.dwCaps = SurfaceCapsFlags.PRIMARYSURFACE | _surfaceFlags;
            return desc;
        }

        private void CreateSoftwareBuffer(BackbufferMode backBufferMode)
        {
            var desc = GetDesc();
            _primarySurface = CreateSurface(desc);

            if (backBufferMode != BackbufferMode.None)
            {
                desc = new DDSURFACEDESC();
                desc.ddsCaps.dwCaps |= _surfaceFlags;
                desc.dwFlags = SurfaceDescFlags.CAPS | SurfaceDescFlags.WIDTH | SurfaceDescFlags.HEIGHT;
                desc.dwHeight = (uint)_primarySurface.Height;
                desc.dwWidth = (uint)_primarySurface.Width;
                ScreenSurface = CreateSurface(desc);
            }
        }

        private void CreateHardwareBuffer()
        {
            var desc = GetDesc();
            
            desc.dwFlags |= SurfaceDescFlags.BACKBUFFERCOUNT;
            desc.ddsCaps.dwCaps |= SurfaceCapsFlags.FLIP;
            desc.dwBackBufferCount = 1;
            _primarySurface = CreateSurface(desc);

            var callback = new EnumSurfacesCallback(EnumAttachSurfacesCallback);
            var callbackPtr = Marshal.GetFunctionPointerForDelegate(callback);

            _primarySurface._surface.EnumAttachedSurfaces(IntPtr.Zero, callbackPtr);

            GC.KeepAlive(callback);
        }

        public DirectDrawGraphics(IntPtr hwnd, BackbufferMode backBufferMode)
        {
            _hwnd = hwnd;

            CreateDirectDraw();
            SetDirectDrawCoopLevel();

            var driverCaps = new DDCAPS();
            var softwareEmulatorCaps = new DDCAPS();

            driverCaps.dwSize = (uint)Marshal.SizeOf(typeof(DDCAPS));
            softwareEmulatorCaps.dwSize = driverCaps.dwSize;

            var result = _ddraw.GetCaps(out driverCaps, out softwareEmulatorCaps);
            if (result != 0)
                throw ExceptionUtil.Create(result);

            _surfaceFlags = ((driverCaps.ddsCaps.dwCaps & SurfaceCapsFlags.VIDEOMEMORY) == SurfaceCapsFlags.VIDEOMEMORY) ? SurfaceCapsFlags.VIDEOMEMORY : SurfaceCapsFlags.SYSTEMMEMORY;

            SetSupportFlip(backBufferMode, driverCaps);

            if(Microsoft.WindowsMobile.DirectX.Direct3D.Manager.Adapters.Default.Information.DriverName.ToLower() == "d3dmref.dll")
            {
                _supportHardwareFlip = false;
                _surfaceFlags = SurfaceCapsFlags.SYSTEMMEMORY;
            }

            if (_supportHardwareFlip && backBufferMode != BackbufferMode.None && backBufferMode != BackbufferMode.Software)
            {
                uint total = 0;
                uint free = 0;

                try
                {
                    _ddraw.GetAvailableVidMem(ref driverCaps.ddsCaps, ref total, ref free);
                    System.Diagnostics.Debug.WriteLine(string.Format("Creating hardware buffer, GetAvailableVidMemory not implemented {0} {1}", total, free));
                }
                catch (NotImplementedException)
                {
                    System.Diagnostics.Debug.WriteLine("Creating hardware buffer, GetAvailableVidMemory not implemented");
                }
                
                CreateHardwareBuffer();
            }
            else
            {
                System.Diagnostics.Debug.WriteLine("Creating software buffer.");
                CreateSoftwareBuffer(backBufferMode);
            }
         
            _screenArea = new Rectangle(0, 0, _primarySurface.Width, _primarySurface.Height);
        }

        public static IntPtr Init()
        {
            return NativeMethods.LoadLibrary("DirectDrawWrapperUnmanaged.dll");
        }

        public void SetNormalCoopMode()
        {
            var result = _ddraw.SetCooperativeLevel(_hwnd, CooperativeFlags.Normal);
            if (result != DD_OK)
                throw ExceptionUtil.Create(result);            
        }

        public void Resize(int width, int height)
        {
            _screenArea = new Rectangle(0, 0, width, height);
        }

        public Surface CreateSurface(int width, int height)
        {
            var desc = new DDSURFACEDESC { dwSize = (uint)Marshal.SizeOf(typeof(DDSURFACEDESC)), dwHeight = (uint)height, dwWidth = (uint)width };
            desc.ddsCaps.dwCaps = _surfaceFlags;
            desc.dwFlags = SurfaceDescFlags.CAPS | SurfaceDescFlags.WIDTH | SurfaceDescFlags.HEIGHT;

            return CreateSurface(desc);
        }

        public Surface CreateSurface(DDSURFACEDESC surfaceDesc)
        {
            var desc = surfaceDesc;
            desc.dwSize = (uint)Marshal.SizeOf(typeof(DDSURFACEDESC));

            IDDrawSurface surface = null;
            try
            {
                var result = _ddraw.CreateSurface(ref desc, out surface, IntPtr.Zero);
                _ddraw.FlipToGDISurface();
                if (result != 0)
                    throw ExceptionUtil.Create(result);
            }
            catch (COMException ex)
            {
                System.Diagnostics.Debug.WriteLine(ex.ErrorCode);
                throw Marshal.GetExceptionForHR(ex.ErrorCode);
            }

            return new Surface(this, surface);
        }

        public Surface CreateSurfaceFromImage(Bitmap image)
        {
            var desc = new DDSURFACEDESC { dwSize = (uint)Marshal.SizeOf(typeof(DDSURFACEDESC)), dwHeight = (uint)image.Height, dwWidth = (uint)image.Width };
            desc.ddsCaps.dwCaps = _surfaceFlags;
            desc.dwFlags = SurfaceDescFlags.CAPS | SurfaceDescFlags.WIDTH | SurfaceDescFlags.HEIGHT;

            var surface = CreateSurface(desc);
            surface.LoadImage(image);
            return surface;
        }

        public void Flip()
        {
            uint retval;
            if (_supportHardwareFlip)
            {
                retval = _primarySurface._surface.Flip(IntPtr.Zero, FlipFlags.WAITNOTBUSY);

                if (retval != 0)
                    throw ExceptionUtil.Create(retval);
            }
            else
            {
                tagRECT dest = _screenArea;

                _primarySurface.Draw(ref dest, ScreenSurface, ref dest, BltFlags.DDBLT_WAITNOTBUSY);
            }
        }

        private uint EnumAttachSurfacesCallback(IDDrawSurface surface, ref DDSURFACEDESC surfaceDesc, IntPtr lpContext)
        {
            ScreenSurface = new Surface(this, surface);
            return 0;
        }

        public Surface ScreenSurface { get; private set; }

        public void Dispose()
        {
            if (_isDisposed)
                return;

            _isDisposed = true;
            OnDispose();
            GC.SuppressFinalize(this);
        }

        private void OnDispose()
        {
            if (_ddraw != null)
                Marshal.FinalReleaseComObject(_ddraw);

            if (_clipper != null)
                Marshal.FinalReleaseComObject(_clipper);

            if (_primarySurface != null)
                _primarySurface.Dispose();

            if (ScreenSurface != null)
                ScreenSurface.Dispose();

            _ddraw = null;
            _clipper = null;
            _primarySurface = null;
            ScreenSurface = null;
        }
    }
}